測試 signals 是軟體開發中非常重要的一部分,但由於時間限制,它很容易被開發團隊忽略。
在應用程式中使用 signals
、computed signals
、 signal inputs
和 effect
時,負責的團隊會編寫測試案例來驗證其正確性。
這是我第一次寫程式碼來測試 signals;因此,我在寫這篇文章時也學到了新東西。
import { computed, Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AppService {
#counter = signal(0);
value = computed(() => this.#counter());
increase(num = 1) {
this.#counter.update((prev) => prev + num);
}
decrease(num = 1) {
this.#counter.update((prev) => prev - num);
}
reset() {
this.#counter.set(0);
}
}
這是一個偏好問題。我將 #counter
signal 和 value
computed signal 封裝在 AppService
服務中,但邏輯也可以在 AppComponent
組件中實現,因為它很簡單。
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
imports: [],
template: `
<p>Child works!</p>
<p data-testId="count">Count: {{ count() }}</p>
<p data-testId="double">Double: {{ double() }}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
count = input(0);
double = computed(() => this.count() * 2);
}
ChildComponent
組件由 count
signal input 和 double
computed signal 組成。
import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core';
import { AppService } from './app.service';
import { ChildComponent } from './child/child.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [ChildComponent],
template: `
<h1>Hello, {{ title }}</h1>
<div>
<p id="value">Value: {{ appService.value() }}</p>
<button id="increase" (click)="increase(1)">Add 1</button>
<button id="increase2" (click)="increase(2)">Add 2</button>
<button id="decrease" (click)="decrease(1)">Decrease</button>
<button id="decrease2" (click)="decrease(2)">Decrease 2</button>
<button id="reset" (click)="reset()">Reset</button>
<app-child [count]="appService.value()" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
title = 'day29-signal-testing';
appService = inject(AppService);
increase(num = 1) {
this.appService.increase(num);
}
decrease(num = 1) {
this.appService.decrease(num);
}
reset() {
this.appService.reset();
}
}
export const buttonClick = <T>(el: DebugElement, fixture: ComponentFixture<T>,
target: HTMLElement, expected: string) => {
el.nativeElement.click();
fixture.detectChanges();
expect(target.textContent).toBe(expected);
}
export const getElement = <T>(fixture: ComponentFixture<T>, key: string): DebugElement => {
return fixture.debugElement.query(By.css(key));
}
在 test/button-test.util.ts
檔案中新增兩個測試用例將重複呼叫的實用函數。 getElement
函數透過 fixture
和 CSS 選擇器
查詢 DebugElement
。 buttonClick
函數使用 HTML 元素執行單擊,觸發 change detection,並比較元素文字是否與預期結果相同。
el.nativeElement.click();
fixture.detectChanges();
該元素執行單擊,然後發生 change detection。
expect(target.textContent).toBe(expected);
將元素文字與預期結果進行比較。如果比較為正確的,則測試通過。否則,測試失敗,我需要修正它。
// app.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ChildComponent } from './child/child.component';
describe('AppComponent', () => {
let fixture: ComponentFixture<AppComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent, ChildComponent],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
});
}
我們可以在 app.component.spec.ts
檔案中找到測試案例。 beforeEach
函數在每個測試案例之前執行,以編譯 AppComponent
組件、初始化 fixture
變數並觸發 change detection。
it('should increase the counter by 1', () => {
const value: HTMLParagraphElement = getElement(fixture, '[id="value"]').nativeElement;
expect(value.textContent).toBe('Value: 0');
const el = getElement(fixture, '[id="increase"]');
buttonClick(el, fixture, value, 'Value: 1');
});
第一個測試案例驗證 #counter
signal 增加 1 並且 value
computed signal 顯示正確的值。 第一個 expect
語句驗證 paragrah 元素 'Value: 0'。 呼叫可重複使用實用函數後,測試程式碼更具可讀性。
const el = getElement(fixture, '[id="increase"]');
buttonClick(el, fixture, value, 'Value: 1');
然後,測試用例查詢 "Add 1" 按鈕並將其傳遞給 buttonClick
函數。此函數點擊按鈕,在執行 change detection 後更新 signal,並驗證元素文字為 'Value:1'。
it('should increase the counter by 2 after 2 button clicks', () => {
const value: HTMLParagraphElement = getElement(fixture, '[id="value"]').nativeElement;
expect(value.textContent).toBe('Value: 0');
const el = getElement(fixture, '[id="increase"]');
buttonClick(el, fixture, value, 'Value: 1');
buttonClick(el, fixture, value, 'Value: 2');
});
此測試案例驗證點擊兩次按鈕後 paragraph 元素是否顯示正確的值。 點擊按鈕並進行 change detection。點擊第一次按鈕後,paragraph 元素顯示 'Value:1'。 再次按一下相同按鈕,會出現另一個 change detection。此元素顯示預期值 'Value:2'。
it('should update the child component', () => {
const value: HTMLParagraphElement = getElement(fixture, '[id="value"]').nativeElement;
expect(value.textContent).toBe('Value: 0');
const el = getElement(fixture, '[id="increase2"]');
buttonClick(el, fixture, value, 'Value: 2');
const count: HTMLParagraphElement = getElement(fixture, '[data-testId="count"]').nativeElement;
const double: HTMLParagraphElement = getElement(fixture, '[data-testId="double"]').nativeElement;
expect(count.textContent).toBe('Count: 2');
expect(double.textContent).toBe('Double: 4');
});
ChildComponent
組件是 AppComponent
組件的子元件。我想驗證 input 更新時子組件顯示正確的值。此測試案例查詢 "Add 2" 按鈕,模擬按鈕單擊和 change detection。 paragraph元素顯示正確的 'Value:2'。
const count: HTMLParagraphElement = getElement(fixture, '[data-testId="count"]').nativeElement;
expect(count.textContent).toBe('Count: 2');
第一行查詢 ChildComponent
顯示 count
signal input 的 paragraph 元素。按鈕點擊後 input 變為2;因此,該元素顯示 'Value:2'。 expect
語句驗證元素文字是否與預期結果相同。
const count: HTMLParagraphElement = getElement(fixture, '[data-testId="double"]').nativeElement;
expect(count.textContent).toBe('Count: 4');
第一行查詢 ChildComponent
顯示 double
computed signal 的 parapgrah 元素。按鈕點擊後輸入變為2;因此, double
computed signal 重新計算為 4。 expect
語句驗證元素文字是否與預期結果相同。
// child.component.spec.ts
let fixture: ComponentFixture<ChildComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChildComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ChildComponent);
fixture.detectChanges();
});
在 child.component.spec.ts
中,我們初始化 fixture
和 component
變數來測試 signal input。
it('should update the signal input', () => {
const count: HTMLParagraphElement = getElement(fixture, '[data-testId="count"]').nativeElement;
const double: HTMLParagraphElement = getElement(fixture, '[data-testId="double"]').nativeElement;
expect(count.textContent).toBe('Count: 0');
expect(double.textContent).toBe('Double: 0');
fixture.componentRef.setInput('count', 1);
fixture.detectChanges();
expect(count.textContent).toBe('Count: 1');
expect(double.textContent).toBe('Double: 2');
});
signal input為 0,因此兩個段落元素最初都顯示 'Count: 0' 和'Double: 0'。
fixture.componentRef.setInput('count', 1);
fixture.detectChanges();
我使用 componentRef.setInput
將 count
input 設為 1,並觸發 change detection。
expect(count.textContent).toBe('Count: 1');
expect(double.textContent).toBe('Double: 2');
count
段落元素顯示 'Value:1'。 double
computed signal 變為 2,因此段落元素顯示 'Double: 2'。
我無法讓 effect
測試用例發揮成功;因此,我不會在這篇文章中包含任何 effect
的測試案例。
signal
、 computed()
、 input()
和 effect。change detection
以更新訊號 (signal)。然後,編寫CSS來查詢HTML元素以驗證結果。鐵人賽的第 29 天到此結束。